<?php

declare(strict_types=1);

namespace app\service\user;

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use think\Request;
use app\model\UserRefreshToken;
use app\service\dto\TokenPair;
use app\service\sys\ConfigService;
use think\facade\Log;

class TokenService
{
    // 访问令牌过期时间（2小时）
    private $accessExpire = 7200;

    // 刷新令牌过期时间（7天）
    private $refreshExpire = 604800;

    // JWT密钥
    private $key = '';
    private $request;
    // 构造函数 依赖注入数据库中的配置key
    public function __construct(ConfigService $configService, Request $request)
    {
        $config = $configService->get('jwt');
        // 从配置中获取，或者使用默认值
        $this->key = $config['key'] ?? 'bearing_shop_default_key';
        $this->accessExpire = $config['access_expire'] ?? 7200;
        $this->refreshExpire = $config['refresh_expire'] ?? 604800;
        $this->request = $request;
    }

    /**
     * 生成访问令牌和刷新令牌
     * @param array $payload 载荷数据
     * @return TokenPair
     */
    public function generateTokens($payload = []): TokenPair
    {
        $currentTime = time();

        // 生成访问令牌
        $accessToken = $this->generateAccessToken($payload);

        // 生成刷新令牌
        $refreshToken = $this->generateRefreshToken($payload);

        // 返回TokenPair对象
        $expires = date('Y-m-d H:i:s', $currentTime + $this->accessExpire);
        return new TokenPair($accessToken, $refreshToken, $expires);
    }

    /**
     * 生成访问令牌
     * @param array $payload 载荷数据
     * @return string
     */
    public function generateAccessToken($payload = []): string
    {
        $currentTime = time();

        // 生成访问令牌
        $accessPayload = array_merge($payload, [
            'exp' => $currentTime + $this->accessExpire,
            'iat' => $currentTime,
            'type' => 'access',
        ]);

        return JWT::encode($accessPayload, $this->key, 'HS256');
    }

    /**
     * 生成刷新令牌
     * @param array $payload 载荷数据
     * @return string
     */
    public function generateRefreshToken($payload = []): string
    {
        $currentTime = time();

        // 创建数据库记录
        $refreshExpiresAtTime = $currentTime + $this->refreshExpire;
        $jti = $this->generateJti($payload);
        $device_fingerprint = $this->request->device_fingerprint;
        // 只有当payload中包含user_id时才创建数据库记录
        if (isset($payload['user_id'])) {
            // 判断设备和 ID
            $token = UserRefreshToken::where('user_id', $payload['user_id'])
                ->where('device_fingerprint', $device_fingerprint)
                ->find();

            if ($token) {
                // 如果找到了现有记录，更新它
                $token->setOperationType('relogin')->save([
                    'jti' => $jti,
                    'expires_time' => date('Y-m-d H:i:s', $refreshExpiresAtTime),
                    'status' => 1,
                    'revoked_time' => null,
                    'revoked_reason' => null,
                ]);
            } else {
                $userRefreshToken = new UserRefreshToken();
                $userRefreshToken->save([
                    'user_id' => $payload['user_id'],
                    'jti' => $jti,
                    'device_fingerprint' => $device_fingerprint,
                    'expires_time' => date('Y-m-d H:i:s', $refreshExpiresAtTime),
                    'status' => 1,
                    'revoked_time' => null,
                    'revoked_reason' => null,
                ]);
            }
        }

        // 生成刷新令牌
        $refreshPayload = array_merge($payload, [
            'jti' => $jti,
            'exp' => $refreshExpiresAtTime,
            'iat' => $currentTime,
            'type' => 'refresh',
        ]);

        return JWT::encode($refreshPayload, $this->key, 'HS256');
    }

    /**
     * 验证访问令牌
     * @param string $token JWT令牌
     * @return array 解码后的载荷
     * @throws \Exception
     */
    public function validateAccessToken($token)
    {
        try {
            $decoded = JWT::decode($token, new Key($this->key, 'HS256'));

            // 检查令牌类型
            if (!isset($decoded->type) || $decoded->type !== 'access') {
                Log::warning("访问令牌类型不正确,expected:access, actual:{actual}", ['expected' => 'access', 'actual' => $decoded->type ?? 'undefined']);
                throw new \Exception('访问令牌类型不匹配', 402);
            }

            // 检查是否过期
            if (!isset($decoded->exp) || $decoded->exp < time()) {
                Log::warning("访问令牌已过期,jti:{jti}, exp:{exp}", ['jti' => $decoded->jti ?? '', 'exp' => $decoded->exp ?? 'undefined']);
                throw new \Exception('访问令牌已过期', 401);
            }

            return (array) $decoded;
        } catch (\Exception $e) {
            Log::error("访问令牌验证失败,error:[{error}], token:[{token}]", ['error' => $e->getMessage(), 'token' => $token]);
            throw new \Exception($e->getMessage(), $e->getCode() ?: 401);
        }
    }

    /**
     * 验证刷新令牌并生成新的访问令牌和刷新令牌
     * @param string $token JWT令牌
     * @return TokenPair 新的令牌对
     * @throws \Exception
     */
    public function validateRefreshToken($token)
    {
        try {
            $decoded = JWT::decode($token, new Key($this->key, 'HS256'));

            // 检查令牌类型
            if (!isset($decoded->type) || $decoded->type !== 'refresh') {
                Log::warning("刷新令牌类型不正确", ['expected' => 'refresh', 'actual' => $decoded->type ?? 'undefined']);
                throw new \Exception('刷新令牌类型不匹配', 402);
            }

            // 检查数据库中是否存在有效的刷新令牌（仅当有jti时）
            $refreshTokenRecord = null;
            if (isset($decoded->jti)) {
                $refreshTokenRecord = UserRefreshToken::where('jti', $decoded->jti)
                    ->where('status', 1)
                    ->whereNull('revoked_time')
                    ->where('expires_time', '>', date('Y-m-d H:i:s'))
                    ->find();

                if (!$refreshTokenRecord) {
                    Log::warning("刷新令牌在数据库中不存在或已失效", ['jti' => $decoded->jti]);
                    throw new \Exception('刷新令牌无效', 401);
                }
            }

            // 检查是否过期
            if (!isset($decoded->exp) || $decoded->exp < time()) {
                Log::warning("刷新令牌已过期", ['jti' => $decoded->jti ?? '', 'exp' => $decoded->exp ?? 'undefined']);
                throw new \Exception('刷新令牌已过期', 401);
            }

            // 生成新的访问令牌
            // 构造访问令牌载荷（移除令牌特定字段）
            $accessPayload = (array) $decoded;
            unset($accessPayload['exp'], $accessPayload['iat'], $accessPayload['type']);
            $accessToken = $this->generateAccessToken($accessPayload);

            // 生成新的刷新令牌
            $refreshPayload = (array) $decoded;
            unset($refreshPayload['exp'], $refreshPayload['iat'], $refreshPayload['type']);

            $currentTime = time();
            $refreshExpiresAtTime = $currentTime + $this->refreshExpire;

            $refreshPayload = array_merge($refreshPayload, [
                'exp' => $refreshExpiresAtTime,
                'iat' => $currentTime,
                'type' => 'refresh',
            ]);

            $newRefreshToken = JWT::encode($refreshPayload, $this->key, 'HS256');

            // 更新数据库中的刷新令牌记录
            if (isset($decoded->jti) && $refreshTokenRecord) {
                $refreshTokenRecord->save([
                    'expires_time' => date('Y-m-d H:i:s', $refreshExpiresAtTime)
                ]);
            }

            // 返回新的TokenPair对象
            $expires = date('Y-m-d H:i:s', $currentTime + $this->accessExpire);
            return new TokenPair($accessToken, $newRefreshToken, $expires);
        } catch (\Exception $e) {
            Log::error("刷新令牌[{token}]验证失败:{error}", ['error' => $e->getMessage(), 'token' => $token]);
            throw new \Exception($e->getMessage(), $e->getCode() ?: 401);
        }
    }

    /**
     * 使令牌失效（直接更新数据库状态）
     * @param string $jti 令牌标识符
     * @return bool
     * @throws \Exception
     */
    public function invalidateTokenByJti($jti)
    {
        // 更新数据库中的记录状态
        $refreshToken = UserRefreshToken::where('jti', $jti)->find();
        if (!$refreshToken) {
            Log::warning("尝试撤销不存在的令牌[{jti}]", ['jti' => $jti]);
            return false;
        }

        $this->revokeToken($refreshToken, 'manual', 'revoke');
        Log::info("令牌[{jti}]已撤销", ['jti' => $jti]);
        return true;
    }

    /**
     * 使用户的所有令牌失效（单点登出）
     * @param int $userId 用户ID
     * @return bool
     */
    public function invalidateUserTokens($userId, $isAll = false)
    {
        $map = [
            ['user_id', '=', $userId],
            ['status', '=', 1],
        ];
        if (!$isAll) {
            $map[] = ['device_fingerprint', '=', $this->request->device_fingerprint];
        }
        // 更新用户所有令牌的状态
        $tokens = UserRefreshToken::where($map)->select();
        $reason = $isAll ? 'logout_all' : 'logout';
        $type = $isAll ? 'logout_all' : 'logout';

        foreach ($tokens as $token) {
            $this->revokeToken($token, $reason, $type);
        }

        Log::info("用户[{user_id}]所有令牌已失效", ['user_id' => $userId]);
        return true;
    }
    /**
     * 获取令牌剩余有效时间
     * @param string $token 令牌
     * @return int 剩余秒数，-1表示已过期或无效
     */
    public function getRemainingTime($token)
    {
        try {
            $decoded = JWT::decode($token, new Key($this->key, 'HS256'));

            if (!isset($decoded->exp)) {
                return -1;
            }

            $remaining = $decoded->exp - time();

            return $remaining > 0 ? $remaining : -1;
        } catch (\Exception $e) {
            return -1;
        }
    }

    /**
     * 生成JTI（JWT ID）
     * @param array $payload 载荷数据
     * @return string
     */
    private function generateJti($payload = []): string
    {
        return md5(serialize($payload) . microtime() . uniqid());
    }

    private function revokeToken(UserRefreshToken $token, string $reason, string $type): void
    {
        $time = date('Y-m-d H:i:s');
        $token->setOperationType($type)->save([
            'status' => 2,
            'revoked_time' => $time,
            'revoked_reason' => $reason,
        ]);
    }
}
